import { reactive, getCurrentInstance } from "vue-demi";
import { getVptd } from "../utils/compat";

function resolveSource(source, type) {
  return typeof type === "function" ? type : source[type];
}

function normalizeMap(map) {
  return Array.isArray(map)
    ? map.map((k) => ({ k, v: k }))
    : Object.keys(map).map((k) => ({ k, v: map[k] }));
}

const createMapState = (_store) => (states) => {
  const res = {};
  const db = normalizeMap(states);
  if (!_store) {
    const instance = getCurrentInstance;
    _store = getVptd(instance);
  }
  for (const k in db) {
    let v = db[k];
    res[k] = function () {
      return typeof v === "function"
        ? v.call(this, _store.state)
        : _store.state[v];
    };
  }
  return res;
};

const mapToMethods = (sourceName, runnerName, _store) => (map) => {
  const res = {};
  const db = normalizeMap(map);
  if (!_store) {
    const instance = getCurrentInstance;
    _store = getVptd(instance);
  }
  for (const k in db) {
    let v = db[k];
    res[k] = function (payload) {
      const source = _store[sourceName];
      const runner = _store[runnerName];
      const actualSource = typeof v === "function" ? v.call(this, source) : v;
      return runner.call(_store, actualSource, payload);
    };
  }
  return res;
};

export default class Store {
  constructor({
    state,
    mutations = {},
    actions = {},
    plugins,
    subscribers = [],
  } = {}) {
    this.vm = reactive({
      data: {
        $$state: typeof state === "function" ? state() : state,
      },
    });
    this.mutations = mutations;
    this.actions = actions;
    this.subscribers = subscribers;

    if (plugins) {
      plugins.forEach((p) => this.use(p));
    }

    this.mapState = createMapState(this);
    this.mapActions = mapToMethods("actions", "dispatch", this);
    this.mapMutations = mapToMethods("mutations", "commit", this);
  }

  get state() {
    return this.vm.data.$$state;
  }

  set state(v) {
    if (process.env.NODE_ENV === "development") {
      throw new Error(
        "[puex] store.state is read-only, use store.replaceState(state) instead"
      );
    }
  }

  $emit(event, ...args) {
    return this.vm.$emit(event, ...args);
  }

  $on(event, callback) {
    return this.vm.$on(event, callback);
  }

  subscribe(sub) {
    this.subscribers.push(sub);
    return () => this.subscribers.splice(this.subscribers.indexOf(sub), 1);
  }

  commit(type, payload) {
    this.subscribers.forEach((sub) => sub({ type, payload }, this.state));
    const mutation = resolveSource(this.mutations, type);
    return mutation && mutation(this.state, payload);
  }

  dispatch(type, payload) {
    const action = resolveSource(this.actions, type);
    const ctx = {
      state: this.state,
      dispatch: this.dispatch.bind(this),
      commit: this.commit.bind(this),
      store: this,
    };
    return Promise.resolve(action && action(ctx, payload));
  }

  use(fn) {
    fn(this);
    return this;
  }

  replaceState(state) {
    this.vm.$data.$$state = state;
    return this;
  }
}

export const mapState = createMapState();
export const mapActions = mapToMethods("actions", "dispatch");
export const mapMutations = mapToMethods("mutations", "commit");
